package image;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfFloat;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Range;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.core.Core.MinMaxLocResult;
import org.opencv.highgui.Highgui;
import org.opencv.imgproc.Imgproc;

public class FormDetector2 {

	public String title;
	
	private static boolean DEBUG = true;
	
	public FormDetector2() {
		System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
	}

	/**
	 * Calcul la lookUpTable d'une forme.
	 * @param image
	 * @return
	 */
	public static double[] computeLUT(String image) {
		long start = System.currentTimeMillis();
		
		int[] LUT_x = new int[360];
		int[] LUT_y = new int[360];
		double[] LUT = new double[360];
		int[] LUT_nb_points = new int[360];

		int bary_i = 0;
		int bary_j = 0;
		int nb_outline_points = 0;

		double ix = 0;
		double iy = 0;
		
		double grad_norm_angle = 0;
		int grad_angle_in_degree = 0;
		double bary_angle_norm = 0;
		
		int dark_shape = -1; // -1 si dark +1 sinon
		double outline_threshold = 0.1;

		// creation des matrices
		Mat m = Highgui.imread(image, Highgui.CV_LOAD_IMAGE_GRAYSCALE);
		Size size = new Size(m.size().width, m.size().height);
		
		Mat draw = Highgui.imread(image, Highgui.CV_LOAD_IMAGE_COLOR);
		Mat grad_x = Mat.zeros(size, CvType.CV_64FC1);
		Mat grad_y  = Mat.zeros(size, CvType.CV_64FC1);
		Mat outline = Mat.zeros(size, CvType.CV_64FC1);
		Mat grad_angle = Mat.zeros(size, CvType.CV_64FC1);
		
		// traitements sur l'image
		m.convertTo(m, CvType.CV_64FC1);
		Imgproc.GaussianBlur(m, m, new Size(9, 9), 0, 0);

		// calcul des gradients
		for(int i=1; i<size.height-1; i++) {
			for(int j=1; j<size.width-1; j++) {
				grad_y.put(i, j, m.get(i+1, j)[0] - m.get(i-1, j)[0]);
				grad_x.put(i, j, m.get(i, j+1)[0] - m.get(i, j-1)[0]);
			}
		}

		// calcul du contour et du barycentre
		for(int i=0; i<size.height; i++) {
			for(int j=0; j<size.width; j++) {
				ix = dark_shape * grad_x.get(i, j)[0];
				iy = dark_shape * grad_y.get(i, j)[0];

				if(Math.pow(ix, 2) + Math.pow(iy, 2) > outline_threshold) {
					outline.put(i, j, 1);
					
					bary_i += j;
					bary_j += i;
					nb_outline_points++;
				}
			}
		}
		bary_i /= nb_outline_points;
		bary_j /= nb_outline_points;

		if(DEBUG) {
			System.out.println("Barycentre\t"+bary_i+" "+bary_j);
			Core.circle(draw, new Point(bary_i, bary_j), 1, new Scalar(0, 0, 255), 3);
		}

		// calcul des lookUpTables
		for(int i=0; i<size.height; i++) {
			for(int j=0; j<size.width; j++) {
				if(outline.get(i, j)[0] == 1) {
					ix = dark_shape * grad_x.get(i, j)[0];
					iy = dark_shape * grad_y.get(i, j)[0];

					grad_norm_angle = Math.atan2(iy,ix)/(2*Math.PI); // direction du gradient
					grad_norm_angle = (grad_norm_angle+1) % 1; // décalage de PI/2
					grad_angle_in_degree = (int) (grad_norm_angle*360); // conversion en degres
					
					grad_angle.put(i, j, grad_norm_angle);
					LUT_x[grad_angle_in_degree] += (j- (int) bary_i);
					LUT_y[grad_angle_in_degree] += (i- (int) bary_j);
					LUT_nb_points[grad_angle_in_degree]++;
				}
			}
		}

		for(int i=0; i<360; i++) {
			if(LUT_nb_points[i]>0) {
				bary_angle_norm = Math.atan2(LUT_y[i]/LUT_nb_points[i], LUT_x[i]/LUT_nb_points[i]) / (2*Math.PI);
				bary_angle_norm = (bary_angle_norm+1) % 1;	
				LUT[i] = bary_angle_norm*360;
			} else {
				LUT[i] = -1;
			}
		}
		
		if(DEBUG) {
			System.out.println("Computing LUT took "+(System.currentTimeMillis()-start)+"ms");
			new LoadImage("image/resultats/test_draw.png", draw, "Original", false, 320, 0);
			new LoadImage("image/resultats/test_outline.png", outline, "Contours", true, 640, 0);
			new LoadImage("image/resultats/test_grad_angle.png", grad_angle, "Direction du gradient", true, 0, 240);
		}

		return LUT;
	}
	
	/**
	 * Sauvegarde la lookUpTable dans un fichier.
	 * @param title
	 * @param LUT
	 */
	public static void saveLUT(String file, double[] LUT) {
		try {
			FileWriter fw = new FileWriter(new File(file));
			BufferedWriter writer = new BufferedWriter(fw);
			for(int i=0; i<360; i++) {
				writer.write(String.valueOf((int) LUT[i]));
				writer.newLine();
			}
			writer.close();
			if(DEBUG) System.out.println("LUT saved correctly at "+file);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Charge une lookUpTable pré-calculée.
	 * @param text
	 * @return
	 */
	public static double[] loadLUT(String text) {
		double[] LUT = new double[360];
		String tmp = "";
		
		try {
			FileReader fr = new FileReader(new File(text));
			BufferedReader reader = new BufferedReader(fr);
			for(int i=0; i<360; i++) {
				tmp = reader.readLine();
				LUT[i] = Integer.valueOf(tmp);
			}
			reader.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return LUT;
	}
	
	/**
	 * Détecte un visage sur une image et la retourne les coordonées de la zone contenant les yeux.
	 * @param image
	 * @return coord du rectangle contenant les yeux
	 */
	public static Point[] findFace(Mat image, String title, Mat draw,
			double[] lut, int blur, int outline_threshold, double r_min, double r_max, int dark_shape) {
		long start = System.currentTimeMillis();
		
		// chargement des images
		Mat m = image.clone();
		Size size = new Size(m.size().width, m.size().height);
		
		Mat grad_x = Mat.zeros(size, CvType.CV_64FC1);
		Mat grad_y = Mat.zeros(size, CvType.CV_64FC1);
		Mat outline = Mat.zeros(size, CvType.CV_64FC1);
		Mat grad_angle = Mat.zeros(size, CvType.CV_64FC1);
		Mat accumulation = Mat.zeros(size, CvType.CV_64FC1);
		
		// variables pour le calcul
		double ix = 0;
		double iy = 0;
		double grad_norm_angle = 0;
		int grad_angle_in_degree = 0;
		double alpha = 0;
		int x = 0;
		int y = 0;
		MinMaxLocResult max = null;
		int rect_size = 0;
		Point rect_1 = null;
		Point rect_2 = null;
		
		// traitement sur l'image de base
		Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2GRAY);
		m.convertTo(m, CvType.CV_64FC1);
		Imgproc.GaussianBlur(m, m, new Size(blur, blur), 0, 0);
		
		// calculer les gradients
		for(int i=1; i<size.height-1; i++) {
			for(int j=1; j<size.width-1; j++) {
				grad_y.put(i, j, m.get(i+1, j)[0] - m.get(i-1, j)[0]);
				grad_x.put(i, j, m.get(i, j+1)[0] - m.get(i, j-1)[0]);
			}
		}
		
		// parcourir les contours
		// pour chaque point calculer l'angle (direction du gradient)
		for(int i=0; i<size.height; i++) {
			for(int j=0; j<size.width; j++) {
				ix = dark_shape * grad_x.get(i, j)[0];
				iy = dark_shape * grad_y.get(i, j)[0];

				if(Math.pow(ix, 2) + Math.pow(iy, 2) > outline_threshold) {
					outline.put(i, j, 1);
	    			
					grad_norm_angle = Math.atan2(iy,ix)/(2*Math.PI);
					grad_norm_angle = (grad_norm_angle+1) % (double) 1;
					grad_angle_in_degree = (int) (grad_norm_angle*360);
					
					grad_angle.put(i, j, grad_norm_angle);
					
					// chercher l'angle alpha avec la LUT
					alpha = getRadian(lut[grad_angle_in_degree]);
					
					// tarcer la droite dans la matrice d'accumulation  entre r min et r max
					for(int k=(int) r_min; k<r_max; k++) {
						x = (int) (j + Math.cos(alpha) * k);
						y = (int) (i + Math.sin(alpha) * k);
						
						if(x >= 0 && y >= 0 && x < size.width && y < size.height)
							accumulation.put(y, x, accumulation.get(y, x)[0]+1);
					}
				}
			}
		}

		// calcul le maximum de la table d'accumulation (le centre de gravité du visage)
		max = Core.minMaxLoc(accumulation);
		Core.circle(draw, max.maxLoc, 1, new Scalar(0, 0, 255), 3);
		
		// encadrer le visage
		rect_size = (int) r_max;
		rect_1 = new Point(max.maxLoc.x-(rect_size/2), max.maxLoc.y-rect_size);
		rect_2 = new Point(max.maxLoc.x+(rect_size/2), max.maxLoc.y+rect_size);
		Core.rectangle(draw, rect_1, rect_2, new Scalar(0, 0, 255));
		
		// encadrer la zone des yeux
		rect_1 = new Point(max.maxLoc.x-(rect_size/2), max.maxLoc.y-(rect_size/2));
		rect_2 = new Point(max.maxLoc.x+(rect_size/2), max.maxLoc.y);
		Core.rectangle(draw, rect_1, rect_2, new Scalar(0, 255, 0));
		
		if(DEBUG) {
			System.out.println("Finding ellipse took "+(System.currentTimeMillis()-start)+"ms");
			new LoadImage("image/resultats/"+title+"_original.png", draw, "Original", false, 0, 0);
			new LoadImage("image/resultats/"+title+"_outline.png", outline, "Contours", true, 640, 0);
			new LoadImage("image/resultats/"+title+"_grad_x.png", grad_x, "Gradient x", true, 640, 0);
			new LoadImage("image/resultats/"+title+"_grad_y.png", grad_y, "Gradient y", true, 640, 0);
			new LoadImage("image/resultats/"+title+"_grad_angle.png", grad_angle, "Direction du gradient", true, 0, 240);
			new LoadImage("image/resultats/"+title+"_acc.png", accumulation, "Accumulation", false, 320, 240);
		}
		
		// evite les depassements
		if(rect_1.x < 0) rect_1.x = 0;
		if(rect_1.y < 0) rect_1.y = 0;
		if(rect_2.x > image.size().width) rect_2.x = image.size().width;
		if(rect_2.y > image.size().height) rect_2.y = image.size().height;
		
		Point[] res = {rect_1, rect_2};
		return res;
	}
	
	public static Point[] findEye(Mat image, Point[] eye_zone, String title, Mat draw,
			double[] lut, int blur, int outline_threshold, double r_min, double r_max, int dark_shape) {
		long start = System.currentTimeMillis();
		
		// charge les matrices
		Mat m = image.clone();
		Size size = new Size(m.size().width, m.size().height);
		
		Mat grad_x = Mat.zeros(size, CvType.CV_64FC1);
		Mat grad_y = Mat.zeros(size, CvType.CV_64FC1);
		Mat outline = Mat.zeros(size, CvType.CV_64FC1);
		Mat grad_angle = Mat.zeros(size, CvType.CV_64FC1);
		Mat accumulation = Mat.zeros(size, CvType.CV_64FC1);
		
		// variables pour les calculs
		double ix = 0;
		double iy = 0;
		double grad_norm_angle = 0;
		int grad_angle_in_degree = 0;
		double alpha = 0;
		int x = 0;
		int y = 0;
		int rect_size = 0;
		Point rect_1 = null;
		Point rect_2 = null;
		
		// traitement de base sur l'image
		Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2GRAY);
		m.convertTo(m, CvType.CV_64FC1);
		Imgproc.GaussianBlur(m, m, new Size(blur, blur), 0, 0);
		
		// calculer les gradients
		for(int i=1; i<size.height-1; i++) {
			for(int j=1; j<size.width-1; j++) {
				grad_y.put(i, j, m.get(i+1, j)[0] - m.get(i-1, j)[0]);
				grad_x.put(i, j, m.get(i, j+1)[0] - m.get(i, j-1)[0]);
			}
		}
		
		// parcourir les contours
		// pour chaque point calculer l'angle (direction du gradient)
		for(int i=0; i<size.height; i++) {
			for(int j=0; j<size.width; j++) {
				ix = dark_shape * grad_x.get(i, j)[0];
				iy = dark_shape * grad_y.get(i, j)[0];

				if(Math.pow(ix, 2) + Math.pow(iy, 2) > outline_threshold
						&& j > eye_zone[0].x && j < eye_zone[1].x
						&& i > eye_zone[0].y && i < eye_zone[1].y) {
					outline.put(i, j, 1);
	    			
					grad_norm_angle = Math.atan2(iy,ix)/(2*Math.PI);
					grad_norm_angle = (grad_norm_angle+1) % (double) 1;
					grad_angle_in_degree = (int) (grad_norm_angle*360);
					
					grad_angle.put(i, j, grad_norm_angle);
					
					// chercher l'angle alpha avec la LUT
					alpha = getRadian(lut[grad_angle_in_degree]);
					
					// tarcer la droite dans la matrice d'accumulation  entre r min et r max
					for(int k=(int) r_min; k<r_max; k++) {
						x = (int) (j + Math.cos(alpha) * k);
						y = (int) (i + Math.sin(alpha) * k);
						
						if(x >= 0 && y >= 0 && x < size.width && y < size.height)
							accumulation.put(y, x, accumulation.get(y, x)[0]+1);
					}
				}
			}
		}

		// calcul le maximum de la table d'accumulation (le centre de gravité du visage)
		MinMaxLocResult max = Core.minMaxLoc(accumulation);
		Core.circle(draw, max.maxLoc, 1, new Scalar(0, 0, 255), 3);
		if(DEBUG) System.out.println("Votes "+max.maxVal);
		
		// encadrer l'oeil
		rect_size = (int) (r_max/2);
		rect_1 = new Point(max.maxLoc.x-rect_size, max.maxLoc.y-rect_size);
		rect_2 = new Point(max.maxLoc.x+rect_size, max.maxLoc.y+rect_size);
		Core.rectangle(draw, rect_1, rect_2, new Scalar(0, 0, 255));
		
		if(DEBUG) {
			System.out.println("Finding disk took "+(System.currentTimeMillis()-start)+"ms");
			new LoadImage("image/resultats/"+title+"_eye_original.png", draw, "Original", false, 0, 0);
			new LoadImage("image/resultats/"+title+"_eye_outline.png", outline, "Contours", true, 640, 0);
			new LoadImage("image/resultats/"+title+"_eye_grad_x.png", grad_x, "Gradient x", true, 640, 0);
			new LoadImage("image/resultats/"+title+"_eye_grad_y.png", grad_y, "Gradient y", true, 640, 0);
			new LoadImage("image/resultats/"+title+"_eye_grad_angle.png", grad_angle, "Direction du gradient", true, 0, 240);
			new LoadImage("image/resultats/"+title+"_eye_acc.png", accumulation, "Accumulation", false, 320, 240);
		}
		
		// evite les depassements
		if(rect_1.x < 0) rect_1.x = 0;
		if(rect_1.y < 0) rect_1.y = 0;
		if(rect_2.x > image.size().width) rect_2.x = image.size().width;
		if(rect_2.y > image.size().height) rect_2.y = image.size().height;

		Point[] res = {rect_1, rect_2};
		return res;
	}
	
	public static Point[] irisRecognitionLD(Mat snapshot, Mat draw, String title) {
		new FormDetector2();
		double[] lut_ellipse = FormDetector2.loadLUT("image/elipse2.lut");
		double[] lut_disk = FormDetector2.loadLUT("image/disque.lut");
		
		// blur, out_line_threshold, r_min, r_max, dark_shape
		Point[] eye_zone = FormDetector2.findFace(snapshot, title, draw, lut_ellipse, 3, 200, 150, 200, -1);
		Point[] eye = FormDetector2.findEye(snapshot, eye_zone, title, draw, lut_disk, 3, 200, 0, 30, -1);
		return eye;
	}
	
	public static Point[] irisRecognitionHD(Mat snapshot, Mat draw, String title) {
		new FormDetector2();
		double[] lut_disk = FormDetector2.loadLUT("image/disque.lut");
		
		// blur, out_line_threshold, r_min, r_max, dark_shape
		Point[] eye_zone = {new Point(0, 0), new Point(snapshot.size().width, snapshot.size().height)};
		
		// marche bien pour une caméra de mauvaise qualitée
		//Point[] eye = FormDetector2.findEye(snapshot, eye_zone, title, draw, lut_disk, 15, 10, 100, 220, -1);
		
		// marche bien avec des photos fixes
		Point[] eye = FormDetector2.findEye(snapshot, eye_zone, title, draw, lut_disk, 9, 200, 0, 100, -1);
		
		return eye;
	}
	
	/**
	 * Retourne l'histogramme de la zone demandée sous la forme:
	 * r1 g1 b1 r2 g2 b2 r3...
	 * @param m
	 * @param zone
	 * @return
	 */
	// TODO convertir l'histo en int/long et non en char
	public static String computeHisto(Mat image, Point[] zone) {
		Mat m = image.clone();
		
		Mat m_sub = new Mat(m, new Range((int) zone[0].y, (int) zone[1].y),
				new Range((int) zone[0].x, (int) zone[1].x)).clone();
		
		List<Mat> m_color_split = new ArrayList<Mat>();
		Core.split(m_sub, m_color_split);
		
		MatOfInt channels = new MatOfInt(0);
		Mat hist_r = new Mat();
		Mat hist_g = new Mat();
		Mat hist_b = new Mat();
		MatOfInt histSize = new MatOfInt(256);

		float range[] = {0, 256};
		MatOfFloat ranges = new MatOfFloat(range);

		Imgproc.calcHist(m_color_split.subList(0, 1), channels, new Mat(), hist_r, histSize, ranges);
		Imgproc.calcHist(m_color_split.subList(1, 2), channels, new Mat(), hist_g, histSize, ranges);
		Imgproc.calcHist(m_color_split.subList(2, 3), channels, new Mat(), hist_b, histSize, ranges);
		
		String res = "";
		for(int i=0; i<256; i++) {
			res = res + (int) hist_r.get(i, 0)[0] + ";";
			res = res + (int) hist_g.get(i, 0)[0] + ";";
			res = res + (int) hist_b.get(i, 0)[0] + ";";
		}
		
		return res;
	}
	
	public static void displayHisto(Mat histo) {
		for(int i=0; i<256; i++)
			System.out.println(i+"\t"+(int) histo.get(i, 0)[0]+"\t"+histo.get(i, 0)[0]);
	}
	
	public static double compareHistoEuclid(String histo1, String histo2) {
		int sum_r = 0;
		int sum_g = 0;
		int sum_b = 0;

		int histo_size = 3*256;
		
		String[] tmp1 = histo1.split(";");
		String[] tmp2 = histo2.split(";");

		int[] histo1_int = new int[histo_size];
		int[] histo2_int = new int[histo_size];
		
		for(int i=0; i<histo_size; i++) {
			histo1_int[i] = Integer.valueOf(tmp1[i]);
			histo2_int[i] = Integer.valueOf(tmp2[i]);
		}
		
		for(int i=0; i<256; i++) {
			sum_r += (histo1_int[i] - histo2_int[i]) * (histo1_int[i] - histo2_int[i]);
			sum_g += (histo1_int[i+1] - histo2_int[i+1]) * (histo1_int[i+1] - histo2_int[i+1]);
			sum_b += (histo1_int[i+2] - histo2_int[i+2]) * (histo1_int[i+2] - histo2_int[i+2]);
		}
		
		return Math.sqrt(sum_r) + Math.sqrt(sum_g) + Math.sqrt(sum_b);
	}
	
	public static double compareHistoBata(String histo1, String histo2) {
		int sum_r = 0;
		int sum_g = 0;
		int sum_b = 0;

		byte[] tmp1 = histo1.getBytes();
		byte[] tmp2 = histo2.getBytes();
		
		for(int i=0; i<256; i++) {
			sum_r += Math.sqrt((int) tmp1[i]*(int) tmp2[i]);
			sum_g += Math.sqrt((int) tmp1[i+1]*(int) tmp2[i+1]);
			sum_b += Math.sqrt((int) tmp1[i+2]*(int) tmp2[i+2]);
		}
		
		if(sum_r < 0.00001 || sum_g < 0.00001 || sum_b < 0.00001)
			return (float) 10;
		
		return -Math.log(sum_r) -Math.log(sum_g) -Math.log(sum_b);
	}
	
	public static double getRadian(double degree) {
		return (Math.PI / 180.0) * degree;
	}
	
	public static void main(String[] args) {
		new FormDetector2();
		
		String image1 = "visage_01";
		Mat snapshot1 = Highgui.imread("image/"+image1+".png", Highgui.CV_LOAD_IMAGE_COLOR);
		Mat untouched1 = snapshot1.clone();
		Mat draw1 = snapshot1.clone();
		
		Point[] eye1 = irisRecognitionLD(snapshot1, draw1, image1);
		
		/*String image1 = "iris_01";
		Mat snapshot1 = Highgui.imread("image/"+image1+".png", Highgui.CV_LOAD_IMAGE_COLOR);
		
		Mat untouched1 = snapshot1.clone();
		Mat draw1 = snapshot1.clone();
		
		Point[] eye1 = FormDetector2.irisRecognitionHD(snapshot1, draw1, image1);
		String histo1 = computeHisto(untouched1, eye1);
		
		
		String image2 = "iris_02";
		Mat snapshot2 = Highgui.imread("image/"+image2+".png", Highgui.CV_LOAD_IMAGE_COLOR);

		Mat untouched2 = snapshot2.clone();
		Mat draw2 = snapshot2.clone();
		
		Point[] eye2 = FormDetector2.irisRecognitionHD(snapshot2, draw2, image2);
		String histo2 = computeHisto(untouched2, eye2);
		
		
		double dist = compareHistoEuclid(histo1, histo2);
		System.out.println("Distance: "+dist);*/
	}

}
